package com.demo.component.base.impl;

import com.demo.component.base.RestTemplateProcessor;
import com.demo.component.base.RestTemplateProcessorRetryableException;
import com.demo.component.base.RestTemplateRetryHandler;
import com.demo.component.base.model.communication.RequestResult;
import com.demo.component.base.model.communication.RequestResultData;
import lombok.SneakyThrows;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.http.*;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestClientResponseException;
import org.springframework.web.client.RestTemplate;

@Service
@Scope("prototype")
@Log4j2
public class RestTemplateProcessorImp implements RestTemplateProcessor {

    static final int MAX_RETRY_ATTEMPTS = 10;
    static final int BACKOFF_DELAY = 10000;

    private RestTemplateRetryHandler restTemplateRetryHandler;
    private RestTemplate restTemplate;

    public RestTemplateProcessorImp(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
        this.restTemplateRetryHandler = null;
    }

    @Autowired
    public RestTemplateProcessorImp(RestTemplate restTemplate, RestTemplateRetryHandler restTemplateRetryHandler) {
        this.restTemplate = restTemplate;
        this.restTemplateRetryHandler = restTemplateRetryHandler;
    }

    public RestTemplate getRestTemplate() {
        return restTemplate;
    }

    @Override
    public void setRestTemplate(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    @SneakyThrows
    @Retryable(maxAttempts = MAX_RETRY_ATTEMPTS,
            retryFor = RestTemplateProcessorRetryableException.class,
            backoff = @Backoff(delay = BACKOFF_DELAY))
    public <TResultDto> RequestResultData<TResultDto> getRestReturningData(String url, HttpHeaders headers, Class<TResultDto> responseType) {
        RequestResultData<TResultDto> result = new RequestResultData<>();
        try {
            ResponseEntity<TResultDto> response = this.executeGetRequest(url, headers, responseType);
            this.fillSuccessData(result, response);
            log.info(String.format("GET %s %s", response.getStatusCode(), this.restTemplate.getUriTemplateHandler().expand(url)));
        } catch (RestClientResponseException ex) {
            if (restTemplateRetryHandler != null && restTemplateRetryHandler.shouldRetry(this.restTemplate.getUriTemplateHandler().expand(url), HttpMethod.GET.name(), ex.getStatusCode(), ex.getMessage())) {
                throw new RestTemplateProcessorRetryableException();
            }
            log.error(String.format("GET %s error: %s ", this.restTemplate.getUriTemplateHandler().expand(url), ex.getMessage()));
            this.fillResultFromHttpException(ex, result);
        } catch (Exception ex) {
            log.error(String.format("GET %s error: %s ", this.restTemplate.getUriTemplateHandler().expand(url), ex.getMessage()));
            this.fillResultFromHttpException(ex, result);
        }

        return  result;
    }

    @Override
    @SneakyThrows
    @Retryable(maxAttempts = MAX_RETRY_ATTEMPTS,
            retryFor = RestTemplateProcessorRetryableException.class,
            backoff = @Backoff(delay = BACKOFF_DELAY))
    public <TRequestDto, TResultDto> RequestResultData<TResultDto> postRestReturningData(TRequestDto requestModel, String url, HttpHeaders httpHeaders, Class<TResultDto> responseType) {
        RequestResultData<TResultDto> result = new RequestResultData<>();
        try {
            HttpEntity<TRequestDto> httpEntity = new HttpEntity<>(requestModel, httpHeaders);
            ResponseEntity<TResultDto> response = restTemplate.exchange(url, HttpMethod.POST, httpEntity, responseType);
            HttpHeaders headers = response.getHeaders();
            result.setHttpHeaders(headers);
            this.fillSuccessData(result, response);
            log.info(String.format("POST %s %s", response.getStatusCode(), this.restTemplate.getUriTemplateHandler().expand(url)));
        } catch (RestClientResponseException ex) {
            if (restTemplateRetryHandler != null && restTemplateRetryHandler.shouldRetry(this.restTemplate.getUriTemplateHandler().expand(url), HttpMethod.POST.name(), ex.getStatusCode(), ex.getMessage())) {
                throw new RestTemplateProcessorRetryableException();
            }
            log.error(String.format("POST %s error: %s ", this.restTemplate.getUriTemplateHandler().expand(url), ex.getMessage()));
            this.fillResultFromHttpException(ex, result);
        } catch(Exception ex){
            log.error(String.format("POST %s error: %s ", this.restTemplate.getUriTemplateHandler().expand(url), ex.getMessage()));
            this.fillResultFromHttpException(ex, result);
        }

        return result;
    }

    @Override
    @SneakyThrows
    @Retryable(maxAttempts = MAX_RETRY_ATTEMPTS,
            retryFor = RestTemplateProcessorRetryableException.class,
            backoff = @Backoff(delay = BACKOFF_DELAY))
    public <T> RequestResult postRestReturningVoid(T requestModel, String url, HttpHeaders httpHeaders) {

        RequestResult result = new RequestResult();
        try {
            HttpEntity<T> httpEntity = new HttpEntity<>(requestModel, httpHeaders);
            ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.POST, httpEntity, String.class);
            String message = response.getBody();
            HttpHeaders headers = response.getHeaders();
            result.setHttpHeaders(headers);
            result.setSuccess(message, this.convertStatusCodeToString(response.getStatusCode()));
            log.info(String.format("POST %s %s", response.getStatusCode(), this.restTemplate.getUriTemplateHandler().expand(url)));
        }
        catch (RestClientResponseException ex) {
            if (restTemplateRetryHandler != null && restTemplateRetryHandler.shouldRetry(this.restTemplate.getUriTemplateHandler().expand(url), HttpMethod.POST.name(), ex.getStatusCode(), ex.getMessage())) {
                throw new RestTemplateProcessorRetryableException();
            }
            log.error(String.format("POST %s error: %s ", this.restTemplate.getUriTemplateHandler().expand(url), ex.getMessage()));
            this.fillResultFromHttpException(ex, result);
        }
        catch(Exception ex){
            log.error(String.format("POST %s error: %s ", this.restTemplate.getUriTemplateHandler().expand(url), ex.getMessage()));
            this.fillResultFromHttpException(ex, result);
        }

        return result;
    }

    @Override
    @SneakyThrows
    @Retryable(maxAttempts = MAX_RETRY_ATTEMPTS,
            retryFor = RestTemplateProcessorRetryableException.class,
            backoff = @Backoff(delay = BACKOFF_DELAY))
    public <T> RequestResult putRestReturningVoid(T requestModel, String url, HttpHeaders httpHeaders) {
        RequestResult result = new RequestResult();
        try {
            HttpEntity<T> httpEntity = new HttpEntity<>(requestModel, httpHeaders);
            ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.PUT, httpEntity, String.class);
            String message = response.getBody();
            HttpHeaders headers = response.getHeaders();
            result.setHttpHeaders(headers);
            result.setSuccess(message, this.convertStatusCodeToString(response.getStatusCode()));
            log.info(String.format("PUT %s %s", response.getStatusCode(), this.restTemplate.getUriTemplateHandler().expand(url)));
        } catch (RestClientResponseException ex) {
            if (restTemplateRetryHandler != null && restTemplateRetryHandler.shouldRetry(this.restTemplate.getUriTemplateHandler().expand(url), HttpMethod.PUT.name(), ex.getStatusCode(), ex.getMessage())) {
                throw new RestTemplateProcessorRetryableException();
            }
            log.error(String.format("PUT %s error: %s ", this.restTemplate.getUriTemplateHandler().expand(url), ex.getMessage()));
            this.fillResultFromHttpException(ex, result);
        }
        catch(Exception ex){
            log.error(String.format("PUT %s error: %s ", this.restTemplate.getUriTemplateHandler().expand(url), ex.getMessage()));
            this.fillResultFromHttpException(ex, result);
        }

        return result;
    }

    @Override
    public <TRequestDto, TResultDto> RequestResultData<TResultDto> putRestReturningData(TRequestDto requestModel, String url, HttpHeaders httpHeaders, Class<TResultDto> responseType) {
        RequestResultData<TResultDto> result = new RequestResultData<>();
        try {
            HttpEntity<TRequestDto> httpEntity = new HttpEntity<>(requestModel, httpHeaders);
            ResponseEntity<TResultDto> response = restTemplate.exchange(url, HttpMethod.PUT, httpEntity, responseType);
            HttpHeaders headers = response.getHeaders();
            result.setHttpHeaders(headers);

            this.fillSuccessData(result, response);

            log.info(String.format("PUT %s %s", response.getStatusCode(), this.restTemplate.getUriTemplateHandler().expand(url)));
        } catch(Exception ex){
            log.error(String.format("PUT %s error: %s ", this.restTemplate.getUriTemplateHandler().expand(url), ex.getMessage()));
            this.fillResultFromHttpException(ex, result);
        }

        return result;
    }

    @Override
    public <TRequestDto, TResultDto> RequestResultData<TResultDto> patchRestReturningData(TRequestDto requestModel, String url, HttpHeaders httpHeaders, Class<TResultDto> responseType) {
        RequestResultData<TResultDto> result = new RequestResultData<>();
        try {
            HttpEntity<TRequestDto> httpEntity = new HttpEntity<>(requestModel, httpHeaders);
            ResponseEntity<TResultDto> response = restTemplate.exchange(url, HttpMethod.PATCH, httpEntity, responseType);
            HttpHeaders headers = response.getHeaders();
            result.setHttpHeaders(headers);

            this.fillSuccessData(result, response);
            log.info(String.format("PATCH %s %s", response.getStatusCode(), this.restTemplate.getUriTemplateHandler().expand(url)));
        } catch(Exception ex){
            log.error(String.format("PATCH %s error: %s ", this.restTemplate.getUriTemplateHandler().expand(url), ex.getMessage()));
            this.fillResultFromHttpException(ex, result);
        }

        return result;
    }

    @Override
    @SneakyThrows
    @Retryable(maxAttempts = MAX_RETRY_ATTEMPTS,
            retryFor = RestTemplateProcessorRetryableException.class,
            backoff = @Backoff(delay = BACKOFF_DELAY))
    public RequestResult executeRestDelete(String url, HttpHeaders httpHeaders) {
        RequestResult result = new RequestResultData<>();
        try {
            HttpEntity<String> httpEntity = new HttpEntity<>(null, httpHeaders);
            ResponseEntity<String> response = restTemplate.exchange(url, HttpMethod.DELETE, httpEntity, String.class);
            String message = response.getBody();

            if (restTemplateRetryHandler != null && restTemplateRetryHandler.shouldRetry(this.restTemplate.getUriTemplateHandler().expand(url), "DELETE",response.getStatusCode(), message)) {
                throw new RestTemplateProcessorRetryableException();
            }

            HttpHeaders headers = response.getHeaders();
            result.setHttpHeaders(headers);
            result.setSuccess(message, this.convertStatusCodeToString(response.getStatusCode()));
            log.info(String.format("DELETE %s %s", response.getStatusCode(), this.restTemplate.getUriTemplateHandler().expand(url)));
        } catch (RestClientResponseException ex) {
            if (restTemplateRetryHandler != null && restTemplateRetryHandler.shouldRetry(this.restTemplate.getUriTemplateHandler().expand(url), HttpMethod.DELETE.name(), ex.getStatusCode(), ex.getMessage())) {
                throw new RestTemplateProcessorRetryableException();
            }
            log.error(String.format("DELETE %s error: %s ", this.restTemplate.getUriTemplateHandler().expand(url), ex.getMessage()));
            this.fillResultFromHttpException(ex, result);
        } catch (Exception ex) {
            log.error(String.format("DELETE %s error: %s ", this.restTemplate.getUriTemplateHandler().expand(url), ex.getMessage()));
            this.fillResultFromHttpException(ex, result);
        }

        return  result;
    }

    @Override
    public <TResultDto> RequestResultData<TResultDto> deleteRestReturningData(String url, HttpHeaders httpHeaders, Class<TResultDto> responseType) {
        RequestResultData<TResultDto> result = new RequestResultData<>();
        try {
            ResponseEntity<TResultDto> response = restTemplate.exchange(url, HttpMethod.DELETE, null, responseType);
            HttpHeaders headers = response.getHeaders();
            result.setHttpHeaders(headers);
            this.fillSuccessData(result, response);
            log.info(String.format("DELETE %s %s", response.getStatusCode(), this.restTemplate.getUriTemplateHandler().expand(url)));
        } catch(Exception ex){
            log.error(String.format("DELETE %s error: %s ", this.restTemplate.getUriTemplateHandler().expand(url), ex.getMessage()));
            this.fillResultFromHttpException(ex, result);
        }

        return result;
    }

    private <TResultDto> ResponseEntity<TResultDto> executeGetRequest(String url, HttpHeaders headers, Class<TResultDto> responseType) {
        HttpEntity<String> requestEntity = new HttpEntity<>("", headers);
        return restTemplate.exchange(url, HttpMethod.GET, requestEntity, responseType);
    }

    private void fillResultFromHttpException(Exception ex, RequestResult result) {
        if (ex instanceof HttpStatusCodeException statusCodeException) {
            this.fillResultFromStatusCodeException(statusCodeException, result);
        } else {
            result.setFailure(ex.getMessage(), this.convertStatusCodeToString(HttpStatus.INTERNAL_SERVER_ERROR));
        }
    }

    private <T> void fillSuccessData(RequestResultData<T> result, ResponseEntity<T> response) {
        T resultEntity = response.getBody();
        HttpStatusCode statusCode = response.getStatusCode();
        result.setHttpHeaders(response.getHeaders());
        result.setResultData(resultEntity, "Operation executed successfully.", this.convertStatusCodeToString(statusCode));
    }

    private void fillResultFromStatusCodeException(HttpStatusCodeException ex, RequestResult result) {
        HttpStatusCode statusCode = ex.getStatusCode();
        String responseBody = ex.getResponseBodyAsString();

        result.setFailure(responseBody, this.convertStatusCodeToString(statusCode));
    }

    private String convertStatusCodeToString(HttpStatusCode statusCode) {
        String httpStatusCodeAsString = statusCode.toString();
        String numericPart = httpStatusCodeAsString.replaceAll("[^0-9]", "");

        int numericStatusCode = 0;
        try {
            numericStatusCode = Integer.parseInt(numericPart);
            log.trace("Numeric Status Code: " + numericStatusCode);
        } catch (NumberFormatException e) {
            numericStatusCode = 500;
            log.error("Failed to parse numeric status code.");
        }

        return String.valueOf(numericStatusCode);
    }
}

